package com.dark.util;

import lombok.extern.slf4j.Slf4j;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;


/**
 * ClassName: SerialNumberUtil
 * Description:序号相关生成工具
 * <p>
 * 实现简诉：
 * 使用list保存随机数，循环生成900个三位数字，然后打乱顺序
 * 每次获取随机数字的时候，在list中区头结点，并做删除。
 * 当list元素被消耗一半的时候，重新生成一批数据，采用异步执行刷新，尽量降低线程阻塞
 * 为了避免同一秒内，遇到两个相同的随机数，在刷新数据前需要休眠1s。
 * <p>
 * 集群下实现全局唯一：每个节点分配一个唯一的机器id做区分，分配机器id的策略来自于：MachineIdConfig
 * <p>
 * 效率自测：单线程 生成5000个6s;多线程500并发生成5000个8s；
 *
 * <p>
 * 被建议策略：借助redis，每生成一个序号，就缓存记录到redis，保持2s失效，每次生成一个序号，就去redis判断吗，若重复就再次生成
 * 弊端：生成序号的流程被迫变成redis串行，集群并发被限制；
 * 难点：分布式锁的控制，每次redis操作，都需要分布式锁控制，当抢锁失败时，没有适合的处理策略，不适合拒绝，也不适合自旋。因此淘汰此策略
 * </p>
 *
 * @author chengluchao
 * @date: 2020/12/24 17:09
 * @since JDK 1.8
 */
@Slf4j
public class SerialNumberUtil {


    /**
     * 刷新池的数量下限
     */
    private static final int EFFECTIVE_SIZE = 100;

    /**
     * 机器id，借助redis分配，保证每个实例节点不重复
     */
    private static int MACHINE_ID;
    /**
     * 随机数 池
     */
    private static volatile List<Integer> randomPool;

    /**
     * 机器编号是否被正规分配
     * MACHINE_ID被redis分配的时候，initialized=true
     */
    private static volatile boolean initialized = false;

    /**
     * The lock protecting all mutators
     */
    static final transient ReentrantLock lock = new ReentrantLock();


    static {
        init();
    }

    /**
     * 初始化随机数池
     */
    private static void init() {
        //饿汉模式，保证第一次的效率
        refreshRandomPool();
    }

    /**
     * 系统id生成
     * 规则：yyyyMMddHHmmss + 4位随机数字
     *
     * @return 系统id
     */
    public static String generateDistributedIdString() {
        return generateDistributedId() + "";
    }
    public static long generateDistributedId() {
        lock.lock();
        try {
            String idSuffix = readMachineId() + "" + getRandom();
            String dateFormat = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
            String idString = dateFormat + idSuffix;
            Long id = Long.parseLong(idString);
            return id;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 刷新数据池
     */
    private static void refreshRandomPool() {
        try {
            Thread.sleep(1000);
            // 创建新的集合，抛弃原有集合
            randomPool = new ArrayList<>(1000);
            //生产所有的三位数
            for (int i = 100; i < 1000; i++) {
                randomPool.add(i);
            }
            //对池中数据洗牌，达到随机效果
            Collections.shuffle(randomPool);
        } catch (Exception e) {
            log.error("刷新数据池失败", e);
        }
    }

    /**
     * 获取两位随机数，绝不重复的
     * <p>
     * 判断池中数量小于100，就刷新数据。目的是增加随机数的不确定性
     * 获取池中的第一个元素，并移除元素，本轮不再获取
     *
     * @return 随机数
     */
    private static int getRandom() {
        if (randomPool.size() < EFFECTIVE_SIZE) {
            refreshRandomPool();
        }
        int random = randomPool.remove(0);
        return random;
    }

    /**
     * 读取机器编号
     * 理想状态下，来自于redis分配
     * 意外情况，如果redis分配失败，就读取一个随机数作为机器编号
     *
     * @return 机器编号
     */
    private static int readMachineId() {
        //如果没有得到redis分配的机器编号，并且机器编号=0，则认为机器编号没有被初始化
        if (!initialized && MACHINE_ID == 0) {
            MACHINE_ID = getRandom() % 10;
        }
        return MACHINE_ID;
    }

    /**
     * 限制机器编号的设置权限
     * 仅当initialized=false的时候可以修改machineId
     *
     * @param machineId 机器编号
     */
    public static void setMachineId(int machineId) {
        if (!initialized) {
            MACHINE_ID = machineId;
            initialized = true;
        }
    }
}
